package org.jplcc;

import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Vector;
import java.util.logging.Logger;

/**
 * <P>
 * The basic service for managing a set of JPLCC drivers.<br>
 * 
 * <P>
 * As part of its initialization, the <code>DriverManager</code> class will
 * attempt to load the driver classes referenced in the "jplcc.drivers" system
 * property. This allows a user to customize the JPLCC Drivers used by their
 * applications. For example in your ~/.hotjava/properties file you might
 * specify:
 * 
 * <code>jplcc.drivers=foo.bah.Driver:wombat.Driver:bad.taste.ourDriver</code>;
 * 
 *<P>
 * The <code>DriverManager</code> methods <code>getConnection</code> and
 * <code>getDrivers</code> support the Java Standard Edition Service Provider
 * mechanism. JPLCC Drivers must include the file
 * <code>META-INF/services/org.jplcc.Driver</code>. This file contains the name
 * of the JPLCC drivers implementation of <code>org.jplcc.Driver</code>. For
 * example, to load the <code>my.jplcc.Driver</code> class, the
 * <code>META-INF/services/org.jplcc.Driver</code> file would contain the entry:
 * 
 * <code>my.jplcc.Driver</code>;
 * 
 * 
 * <P>
 * When the method <code>getConnection</code> is called, the
 * <code>DriverManager</code> will attempt to locate a suitable driver from
 * amongst those loaded at initialization and those loaded explicitly using the
 * same classloader as the current application.
 * 
 * @see Driver
 * @see Connection
 */

public class DriverManager {

    private static Logger logger = Logger.getLogger("jplcc");
    private static boolean initialized = false;
    private static Vector<DriverInfo> drivers = new Vector<DriverInfo>();

    // Class initialization.
    static void initialize() {
	if (initialized == false) {
	    initialized = true;
	    loadInitialDrivers();
	    logger.fine("JPLCC DriverManager initialized");
	}
    }

    private DriverManager() {
    };

    /**
     * Attempts to establish a connection to the given PLC URL. The
     * <code>DriverManager</code> attempts to select an appropriate driver from
     * the set of registered JPLCC drivers.
     * 
     * @param url
     *            a PLC url of the form
     *            <code> jplcc:<em>subprotocol</em>:<em>subname</em></code>
     * @return a connection to the URL
     * @exception JPLCCException
     *                if a PLC access error occurs
     */
    public static Connection getConnection(String url) throws JPLCCException {

	if (url == null) {
	    throw new JPLCCException("The url cannot be null");
	}

	logger.info("DriverManager.getConnection(\"" + url + "\")");

	if (!initialized) {
	    initialize();
	}

	// Walk through the loaded drivers attempting to make a connection.
	// Remember the first exception that gets raised so we can reraise it.
	JPLCCException reason = null;
	for (int i = 0; i < drivers.size(); i++) {
	    DriverInfo di = (DriverInfo) drivers.elementAt(i);

	    try {
		logger.fine("Trying " + di);
		Connection result = di.driver.connect(url);
		if (result != null) {
		    // Success!
		    logger.fine("getConnection returning " + di);
		    return (result);
		}
	    } catch (JPLCCException ex) {
		if (reason == null) {
		    reason = ex;
		}
	    }
	}

	// if we got here nobody could connect.
	if (reason != null) {
	    logger.info("getConnection failed: " + reason);
	    throw reason;
	}

	logger.info("getConnection: no suitable driver found for " + url);
	throw new JPLCCException("No suitable driver found for " + url);
    }

    /**
     * Attempts to locate a driver that understands the given URL. The
     * <code>DriverManager</code> attempts to select an appropriate driver from
     * the set of registered JPLCC drivers.
     * 
     * @param url
     *            a PLC URL of the form
     *            <code>jplcc:<em>subprotocol</em>:<em>subname</em></code>
     * @return a <code>Driver</code> object representing a driver that can
     *         connect to the given URL
     * @exception JPLCCException
     *                if a PLC access error occurs
     */
    public static Driver getDriver(String url) throws JPLCCException {

	logger.info("DriverManager.getDriver(\"" + url + "\")");

	if (!initialized) {
	    initialize();
	}

	// Walk through the loaded drivers attempting to locate someone
	// who understands the given URL.
	for (int i = 0; i < drivers.size(); i++) {
	    DriverInfo di = (DriverInfo) drivers.elementAt(i);
	    // If the caller does not have permission to load the driver then
	    // skip it.
	    if (getCallerClass(di.driverClassName) != di.driverClass) {
		logger.fine("Skipping: " + di);
		continue;
	    }
	    try {
		logger.fine("Trying " + di);
		if (di.driver.acceptsURL(url)) {
		    // Success!
		    logger.fine("getDriver returning " + di);
		    return (di.driver);
		}
	    } catch (JPLCCException ex) {
		// Drop through and try the next driver.
	    }
	}

	logger.info("getDriver: no suitable driver");
	throw new JPLCCException("No suitable driver");
    }

    /**
     * Registers the given driver with the <code>DriverManager</code>. A
     * newly-loaded driver class should call the method
     * <code>registerDriver</code> to make itself known to the
     * <code>DriverManager</code>.
     * 
     * @param driver
     *            the new JPLCC Driver that is to be registered with the
     *            <code>DriverManager</code>
     * @exception JPLCCException
     *                if a PLC access error occurs
     */
    public static synchronized void registerDriver(Driver driver)
	    throws JPLCCException {
	if (!initialized) {
	    initialize();
	}

	DriverInfo driverInfo = new DriverInfo();

	driverInfo.driver = driver;
	driverInfo.driverClass = driver.getClass();
	driverInfo.driverClassName = driverInfo.driverClass.getName();

	drivers.addElement(driverInfo);
	logger.fine("registerDriver: " + driverInfo);
    }

    /**
     * Drops a driver from the <code>DriverManager</code>'s list.
     * 
     * @param driver
     *            the JPLCC Driver to drop
     * @exception JPLCCException
     *                if a PLC access error occurs
     */
    public static synchronized void deregisterDriver(Driver driver)
	    throws JPLCCException {
	logger.fine("DriverManager.deregisterDriver: " + driver);

	// Walk through the loaded drivers.
	int i;
	DriverInfo di = null;
	for (i = 0; i < drivers.size(); i++) {
	    di = drivers.elementAt(i);
	    if (di.driver == driver) {
		break;
	    }
	}
	// If we can't find the driver just return.
	if (i >= drivers.size()) {
	    logger.fine("    couldn't find driver to unload");
	    return;
	}

	// If the caller does not have permission to load the driver then
	// throw a security exception.
	if (getCallerClass(di.driverClassName) != di.driverClass) {
	    throw new SecurityException();
	}

	// Remove the driver. Other entries in drivers get shuffled down.
	drivers.removeElementAt(i);
    }

    /**
     * Retrieves an Enumeration with all of the currently loaded JPLCC drivers
     * to which the current caller has access.
     * 
     * <P>
     * <B>Note:</B> The classname of a driver can be found using
     * <CODE>d.getClass().getName()</CODE>
     * 
     * @return the list of JPLCC Drivers loaded by the caller's class loader
     */
    @SuppressWarnings("unchecked")
    public static List<Driver> getDrivers() {

	return (List<Driver>) drivers.clone();
    }

    // ------------------------------------------------------------------------

    // Returns the class object that would be created if the code calling the
    // driver manager had loaded the driver class, or null if the class
    // is inaccessible.
    private static Class<?> getCallerClass(String driverClassName) {
	Class<?> callerC = null;

	try {
	    callerC = Class.forName(driverClassName, true, Thread
		    .currentThread().getContextClassLoader());
	} catch (Exception ex) {
	    callerC = null; // being very careful
	}

	return callerC;
    }

    private static void loadInitialDrivers() {

	// If the driver is packaged as a Service Provider,
	// load it.

	// Get all the drivers through the classloader
	// exposed as a org.jplcc.Driver.class service.

	ServiceLoader<Driver> driverServices = ServiceLoader.load(Driver.class);

	/*
	 * Load these drivers, so that they can be instantiated. It may be the
	 * case that the driver class may not be there i.e. there may be a
	 * packaged driver with the service class as implementation of
	 * org.jplcc.Driver but the actual class may be missing. In that case a
	 * Exception will be thrown at runtime by the VM trying to locate and
	 * load the service.
	 * 
	 * Adding a try catch block to catch those runtime errors if driver not
	 * available in classpath but it's packaged as service and that service
	 * is there in classpath.
	 */

	try {
	    Iterator<Driver> driverIterator = driverServices.iterator();
	    while (driverIterator.hasNext()) {
		Driver driver = driverIterator.next();
		logger.fine("DriverManager.initialize: driver = " + driver);
	    } // end while
	} catch (Throwable t) {
	    // Do nothing
	}

	String drivers = System.getProperty("jplcc.drivers");

	if (drivers != null) {
	    while (drivers.length() != 0) {
		int x = drivers.indexOf(':');
		String driver;
		if (x < 0) {
		    driver = drivers;
		    drivers = "";
		} else {
		    driver = drivers.substring(0, x);
		    drivers = drivers.substring(x + 1);
		}
		if (driver.length() == 0) {
		    continue;
		}
		try {
		    logger.fine("DriverManager.Initialize: loading " + driver);
		    Class.forName(driver, true, ClassLoader
			    .getSystemClassLoader());
		} catch (Exception ex) {
		    logger.info("DriverManager.Initialize: load failed: " + ex);
		}
	    }
	}
    }
}

class DriverInfo {
	Driver driver;
	Class<? extends Driver> driverClass;
	String driverClassName;

	public String toString() {
	    return ("driver[className=" + driverClassName + "," + driver + "]");
	}
}
